
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

/**
 * AUTO-GENERATED FILE. DO NOT MODIFY.
 */

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// Universal transitions that can animate between any shapes(series) and any properties in any amounts.
import { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series.js';
import { createHashMap, each, map, filter, isArray, extend } from 'zrender/lib/core/util.js';
import { applyMorphAnimation, getPathList } from './morphTransitionHelper.js';
import Path from 'zrender/lib/graphic/Path.js';
import { initProps } from '../util/graphic.js';
import DataDiffer from '../data/DataDiffer.js';
import { makeInner, normalizeToArray } from '../util/model.js';
import { warn } from '../util/log.js';
import { getAnimationConfig, getOldStyle } from './basicTransition.js';
import Displayable from 'zrender/lib/graphic/Displayable.js';
var DATA_COUNT_THRESHOLD = 1e4;

var getUniversalTransitionGlobalStore = makeInner();

function getGroupIdDimension(data) {
	var dimensions = data.dimensions;

	for (var i = 0; i < dimensions.length; i++) {
		var dimInfo = data.getDimensionInfo(dimensions[i]);

		if (dimInfo && dimInfo.otherDims.itemGroupId === 0) {
			return dimensions[i];
		}
	}
}

function flattenDataDiffItems(list) {
	var items = [];
	each(list, function (seriesInfo) {
		var data = seriesInfo.data;

		if (data.count() > DATA_COUNT_THRESHOLD) {
			if (process.env.NODE_ENV !== 'production') {
				warn('Universal transition is disabled on large data > 10k.');
			}

			return;
		}

		var indices = data.getIndices();
		var groupDim = getGroupIdDimension(data);

		for (var dataIndex = 0; dataIndex < indices.length; dataIndex++) {
			items.push({
				dataGroupId: seriesInfo.dataGroupId,
				data: data,
				dim: seriesInfo.dim || groupDim,
				divide: seriesInfo.divide,
				dataIndex: dataIndex
			});
		}
	});
	return items;
}

function fadeInElement(newEl, newSeries, newIndex) {
	newEl.traverse(function (el) {
		if (el instanceof Path) {
			// TODO use fade in animation for target element.
			initProps(el, {
				style: {
					opacity: 0
				}
			}, newSeries, {
				dataIndex: newIndex,
				isFrom: true
			});
		}
	});
}

function removeEl(el) {
	if (el.parent) {
		// Bake parent transform to element.
		// So it can still have proper transform to transition after it's removed.
		var computedTransform = el.getComputedTransform();
		el.setLocalTransform(computedTransform);
		el.parent.remove(el);
	}
}

function stopAnimation(el) {
	el.stopAnimation();

	if (el.isGroup) {
		el.traverse(function (child) {
			child.stopAnimation();
		});
	}
}

function animateElementStyles(el, dataIndex, seriesModel) {
	var animationConfig = getAnimationConfig('update', seriesModel, dataIndex);
	animationConfig && el.traverse(function (child) {
		if (child instanceof Displayable) {
			var oldStyle = getOldStyle(child);

			if (oldStyle) {
				child.animateFrom({
					style: oldStyle
				}, animationConfig);
			}
		}
	});
}

function isAllIdSame(oldDiffItems, newDiffItems) {
	var len = oldDiffItems.length;

	if (len !== newDiffItems.length) {
		return false;
	}

	for (var i = 0; i < len; i++) {
		var oldItem = oldDiffItems[i];
		var newItem = newDiffItems[i];

		if (oldItem.data.getId(oldItem.dataIndex) !== newItem.data.getId(newItem.dataIndex)) {
			return false;
		}
	}

	return true;
}

function transitionBetween(oldList, newList, api) {
	var oldDiffItems = flattenDataDiffItems(oldList);
	var newDiffItems = flattenDataDiffItems(newList);

	function updateMorphingPathProps(from, to, rawFrom, rawTo, animationCfg) {
		if (rawFrom || from) {
			to.animateFrom({
				style: rawFrom && rawFrom !== from ? // dividingMethod like clone may override the style(opacity)
				// So extend it to raw style.
					extend(extend({}, rawFrom.style), from.style) : from.style
			}, animationCfg);
		}
	}

	function findKeyDim(items) {
		for (var i = 0; i < items.length; i++) {
			if (items[i].dim) {
				return items[i].dim;
			}
		}
	}

	var oldKeyDim = findKeyDim(oldDiffItems);
	var newKeyDim = findKeyDim(newDiffItems);
	var hasMorphAnimation = false;

	function createKeyGetter(isOld, onlyGetId) {
		return function (diffItem) {
			var data = diffItem.data;
			var dataIndex = diffItem.dataIndex; // TODO if specified dim

			if (onlyGetId) {
				return data.getId(dataIndex);
			} // Use group id as transition key by default.
			// So we can achieve multiple to multiple animation like drilldown / up naturally.
			// If group id not exits. Use id instead. If so, only one to one transition will be applied.

			var dataGroupId = diffItem.dataGroupId; // If specified key dimension(itemGroupId by default). Use this same dimension from other data.
			// PENDING: If only use key dimension of newData.

			var keyDim = isOld ? oldKeyDim || newKeyDim : newKeyDim || oldKeyDim;
			var dimInfo = keyDim && data.getDimensionInfo(keyDim);
			var dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta;

			if (dimInfo) {
				// Get from encode.itemGroupId.
				var key = data.get(dimInfo.name, dataIndex);

				if (dimOrdinalMeta) {
					return dimOrdinalMeta.categories[key] || key + '';
				}

				return key + '';
			} // Get groupId from raw item. { groupId: '' }

			var itemVal = data.getRawDataItem(dataIndex);

			if (itemVal && itemVal.groupId) {
				return itemVal.groupId + '';
			}

			return dataGroupId || data.getId(dataIndex);
		};
	} // Use id if it's very likely to be an one to one animation
	// It's more robust than groupId
	// TODO Check if key dimension is specified.

	var useId = isAllIdSame(oldDiffItems, newDiffItems);
	var isElementStillInChart = {};

	if (!useId) {
		// We may have different diff strategy with basicTransition if we use other dimension as key.
		// If so, we can't simply check if oldEl is same with newEl. We need a map to check if oldEl is still being used in the new chart.
		// We can't use the elements that already being morphed. Let it keep it's original basic transition.
		for (var i = 0; i < newDiffItems.length; i++) {
			var newItem = newDiffItems[i];
			var el = newItem.data.getItemGraphicEl(newItem.dataIndex);

			if (el) {
				isElementStillInChart[el.id] = true;
			}
		}
	}

	function updateOneToOne(newIndex, oldIndex) {
		var oldItem = oldDiffItems[oldIndex];
		var newItem = newDiffItems[newIndex];
		var newSeries = newItem.data.hostModel; // TODO Mark this elements is morphed and don't morph them anymore

		var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex);
		var newEl = newItem.data.getItemGraphicEl(newItem.dataIndex); // Can't handle same elements.

		if (oldEl === newEl) {
			newEl && animateElementStyles(newEl, newItem.dataIndex, newSeries);
			return;
		}

		if ( // We can't use the elements that already being morphed
			oldEl && isElementStillInChart[oldEl.id]) {
			return;
		}

		if (newEl) {
			// TODO: If keep animating the group in case
			// some of the elements don't want to be morphed.
			// TODO Label?
			stopAnimation(newEl);

			if (oldEl) {
				stopAnimation(oldEl); // If old element is doing leaving animation. stop it and remove it immediately.

				removeEl(oldEl);
				hasMorphAnimation = true;
				applyMorphAnimation(getPathList(oldEl), getPathList(newEl), newItem.divide, newSeries, newIndex, updateMorphingPathProps);
			} else {
				fadeInElement(newEl, newSeries, newIndex);
			}
		} // else keep oldEl leaving animation.
	}

	new DataDiffer(oldDiffItems, newDiffItems, createKeyGetter(true, useId), createKeyGetter(false, useId), null, 'multiple').update(updateOneToOne).updateManyToOne(function (newIndex, oldIndices) {
		var newItem = newDiffItems[newIndex];
		var newData = newItem.data;
		var newSeries = newData.hostModel;
		var newEl = newData.getItemGraphicEl(newItem.dataIndex);
		var oldElsList = filter(map(oldIndices, function (idx) {
			return oldDiffItems[idx].data.getItemGraphicEl(oldDiffItems[idx].dataIndex);
		}), function (oldEl) {
			return oldEl && oldEl !== newEl && !isElementStillInChart[oldEl.id];
		});

		if (newEl) {
			stopAnimation(newEl);

			if (oldElsList.length) {
				// If old element is doing leaving animation. stop it and remove it immediately.
				each(oldElsList, function (oldEl) {
					stopAnimation(oldEl);
					removeEl(oldEl);
				});
				hasMorphAnimation = true;
				applyMorphAnimation(getPathList(oldElsList), getPathList(newEl), newItem.divide, newSeries, newIndex, updateMorphingPathProps);
			} else {
				fadeInElement(newEl, newSeries, newItem.dataIndex);
			}
		} // else keep oldEl leaving animation.
	}).updateOneToMany(function (newIndices, oldIndex) {
		var oldItem = oldDiffItems[oldIndex];
		var oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); // We can't use the elements that already being morphed

		if (oldEl && isElementStillInChart[oldEl.id]) {
			return;
		}

		var newElsList = filter(map(newIndices, function (idx) {
			return newDiffItems[idx].data.getItemGraphicEl(newDiffItems[idx].dataIndex);
		}), function (el) {
			return el && el !== oldEl;
		});
		var newSeris = newDiffItems[newIndices[0]].data.hostModel;

		if (newElsList.length) {
			each(newElsList, function (newEl) {
				return stopAnimation(newEl);
			});

			if (oldEl) {
				stopAnimation(oldEl); // If old element is doing leaving animation. stop it and remove it immediately.

				removeEl(oldEl);
				hasMorphAnimation = true;
				applyMorphAnimation(getPathList(oldEl), getPathList(newElsList), oldItem.divide, // Use divide on old.
					newSeris, newIndices[0], updateMorphingPathProps);
			} else {
				each(newElsList, function (newEl) {
					return fadeInElement(newEl, newSeris, newIndices[0]);
				});
			}
		} // else keep oldEl leaving animation.
	}).updateManyToMany(function (newIndices, oldIndices) {
		// If two data are same and both have groupId.
		// Normally they should be diff by id.
		new DataDiffer(oldIndices, newIndices, function (rawIdx) {
			return oldDiffItems[rawIdx].data.getId(oldDiffItems[rawIdx].dataIndex);
		}, function (rawIdx) {
			return newDiffItems[rawIdx].data.getId(newDiffItems[rawIdx].dataIndex);
		}).update(function (newIndex, oldIndex) {
			// Use the original index
			updateOneToOne(newIndices[newIndex], oldIndices[oldIndex]);
		}).execute();
	}).execute();

	if (hasMorphAnimation) {
		each(newList, function (_a) {
			var data = _a.data;
			var seriesModel = data.hostModel;
			var view = seriesModel && api.getViewOfSeriesModel(seriesModel);
			var animationCfg = getAnimationConfig('update', seriesModel, 0); // use 0 index.

			if (view && seriesModel.isAnimationEnabled() && animationCfg && animationCfg.duration > 0) {
				view.group.traverse(function (el) {
					if (el instanceof Path && !el.animators.length) {
						// We can't accept there still exists element that has no animation
						// if universalTransition is enabled
						el.animateFrom({
							style: {
								opacity: 0
							}
						}, animationCfg);
					}
				});
			}
		});
	}
}

function getSeriesTransitionKey(series) {
	var seriesKey = series.getModel('universalTransition').get('seriesKey');

	if (!seriesKey) {
		// Use series id by default.
		return series.id;
	}

	return seriesKey;
}

function convertArraySeriesKeyToString(seriesKey) {
	if (isArray(seriesKey)) {
		// Order independent.
		return seriesKey.sort().join(',');
	}

	return seriesKey;
}

function getDivideShapeFromData(data) {
	if (data.hostModel) {
		return data.hostModel.getModel('universalTransition').get('divideShape');
	}
}

function findTransitionSeriesBatches(globalStore, params) {
	var updateBatches = createHashMap();
	var oldDataMap = createHashMap(); // Map that only store key in array seriesKey.
	// Which is used to query the old data when transition from one to multiple series.

	var oldDataMapForSplit = createHashMap();
	each(globalStore.oldSeries, function (series, idx) {
		var oldDataGroupId = globalStore.oldDataGroupIds[idx];
		var oldData = globalStore.oldData[idx];
		var transitionKey = getSeriesTransitionKey(series);
		var transitionKeyStr = convertArraySeriesKeyToString(transitionKey);
		oldDataMap.set(transitionKeyStr, {
			dataGroupId: oldDataGroupId,
			data: oldData
		});

		if (isArray(transitionKey)) {
			// Same key can't in different array seriesKey.
			each(transitionKey, function (key) {
				oldDataMapForSplit.set(key, {
					key: transitionKeyStr,
					dataGroupId: oldDataGroupId,
					data: oldData
				});
			});
		}
	});

	function checkTransitionSeriesKeyDuplicated(transitionKeyStr) {
		if (updateBatches.get(transitionKeyStr)) {
			warn('Duplicated seriesKey in universalTransition ' + transitionKeyStr);
		}
	}

	each(params.updatedSeries, function (series) {
		if (series.isUniversalTransitionEnabled() && series.isAnimationEnabled()) {
			var newDataGroupId = series.get('dataGroupId');
			var newData = series.getData();
			var transitionKey = getSeriesTransitionKey(series);
			var transitionKeyStr = convertArraySeriesKeyToString(transitionKey); // Only transition between series with same id.

			var oldData = oldDataMap.get(transitionKeyStr); // string transition key is the best match.

			if (oldData) {
				if (process.env.NODE_ENV !== 'production') {
					checkTransitionSeriesKeyDuplicated(transitionKeyStr);
				} // TODO check if data is same?

				updateBatches.set(transitionKeyStr, {
					oldSeries: [{
						dataGroupId: oldData.dataGroupId,
						divide: getDivideShapeFromData(oldData.data),
						data: oldData.data
					}],
					newSeries: [{
						dataGroupId: newDataGroupId,
						divide: getDivideShapeFromData(newData),
						data: newData
					}]
				});
			} else {
				// Transition from multiple series.
				if (isArray(transitionKey)) {
					if (process.env.NODE_ENV !== 'production') {
						checkTransitionSeriesKeyDuplicated(transitionKeyStr);
					}

					var oldSeries_1 = [];
					each(transitionKey, function (key) {
						var oldData = oldDataMap.get(key);

						if (oldData.data) {
							oldSeries_1.push({
								dataGroupId: oldData.dataGroupId,
								divide: getDivideShapeFromData(oldData.data),
								data: oldData.data
							});
						}
					});

					if (oldSeries_1.length) {
						updateBatches.set(transitionKeyStr, {
							oldSeries: oldSeries_1,
							newSeries: [{
								dataGroupId: newDataGroupId,
								data: newData,
								divide: getDivideShapeFromData(newData)
							}]
						});
					}
				} else {
					// Try transition to multiple series.
					var oldData_1 = oldDataMapForSplit.get(transitionKey);

					if (oldData_1) {
						var batch = updateBatches.get(oldData_1.key);

						if (!batch) {
							batch = {
								oldSeries: [{
									dataGroupId: oldData_1.dataGroupId,
									data: oldData_1.data,
									divide: getDivideShapeFromData(oldData_1.data)
								}],
								newSeries: []
							};
							updateBatches.set(oldData_1.key, batch);
						}

						batch.newSeries.push({
							dataGroupId: newDataGroupId,
							data: newData,
							divide: getDivideShapeFromData(newData)
						});
					}
				}
			}
		}
	});
	return updateBatches;
}

function querySeries(series, finder) {
	for (var i = 0; i < series.length; i++) {
		var found = finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex || finder.seriesId != null && finder.seriesId === series[i].id;

		if (found) {
			return i;
		}
	}
}

function transitionSeriesFromOpt(transitionOpt, globalStore, params, api) {
	var from = [];
	var to = [];
	each(normalizeToArray(transitionOpt.from), function (finder) {
		var idx = querySeries(globalStore.oldSeries, finder);

		if (idx >= 0) {
			from.push({
				dataGroupId: globalStore.oldDataGroupIds[idx],
				data: globalStore.oldData[idx],
				// TODO can specify divideShape in transition.
				divide: getDivideShapeFromData(globalStore.oldData[idx]),
				dim: finder.dimension
			});
		}
	});
	each(normalizeToArray(transitionOpt.to), function (finder) {
		var idx = querySeries(params.updatedSeries, finder);

		if (idx >= 0) {
			var data = params.updatedSeries[idx].getData();
			to.push({
				dataGroupId: globalStore.oldDataGroupIds[idx],
				data: data,
				divide: getDivideShapeFromData(data),
				dim: finder.dimension
			});
		}
	});

	if (from.length > 0 && to.length > 0) {
		transitionBetween(from, to, api);
	}
}

export function installUniversalTransition(registers) {
	registers.registerUpdateLifecycle('series:beforeupdate', function (ecMOdel, api, params) {
		each(normalizeToArray(params.seriesTransition), function (transOpt) {
			each(normalizeToArray(transOpt.to), function (finder) {
				var series = params.updatedSeries;

				for (var i = 0; i < series.length; i++) {
					if (finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex || finder.seriesId != null && finder.seriesId === series[i].id) {
						series[i][SERIES_UNIVERSAL_TRANSITION_PROP] = true;
					}
				}
			});
		});
	});
	registers.registerUpdateLifecycle('series:transition', function (ecModel, api, params) {
		// TODO api provide an namespace that can save stuff per instance
		var globalStore = getUniversalTransitionGlobalStore(api); // TODO multiple to multiple series.

		if (globalStore.oldSeries && params.updatedSeries && params.optionChanged) {
			// Use give transition config if its' give;
			var transitionOpt = params.seriesTransition;

			if (transitionOpt) {
				each(normalizeToArray(transitionOpt), function (opt) {
					transitionSeriesFromOpt(opt, globalStore, params, api);
				});
			} else {
				// Else guess from series based on transition series key.
				var updateBatches_1 = findTransitionSeriesBatches(globalStore, params);
				each(updateBatches_1.keys(), function (key) {
					var batch = updateBatches_1.get(key);
					transitionBetween(batch.oldSeries, batch.newSeries, api);
				});
			} // Reset

			each(params.updatedSeries, function (series) {
				// Reset;
				if (series[SERIES_UNIVERSAL_TRANSITION_PROP]) {
					series[SERIES_UNIVERSAL_TRANSITION_PROP] = false;
				}
			});
		} // Save all series of current update. Not only the updated one.

		var allSeries = ecModel.getSeries();
		var savedSeries = globalStore.oldSeries = [];
		var savedDataGroupIds = globalStore.oldDataGroupIds = [];
		var savedData = globalStore.oldData = [];

		for (var i = 0; i < allSeries.length; i++) {
			var data = allSeries[i].getData(); // Only save the data that can have transition.
			// Avoid large data costing too much extra memory

			if (data.count() < DATA_COUNT_THRESHOLD) {
				savedSeries.push(allSeries[i]);
				savedDataGroupIds.push(allSeries[i].get('dataGroupId'));
				savedData.push(data);
			}
		}
	});
}